/****

    activequant - activestocks.eu

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

	
	contact  : contact@activestocks.eu
    homepage : http://www.activestocks.eu

 ****/
package org.activequant.broker;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import org.activequant.core.domainmodel.account.Order;
import org.activequant.core.domainmodel.events.OrderCancelEvent;
import org.activequant.core.domainmodel.events.OrderCommissionEvent;
import org.activequant.core.domainmodel.events.OrderCompletionEvent;
import org.activequant.core.domainmodel.events.OrderEvent;
import org.activequant.core.domainmodel.events.OrderExecutionEvent;
import org.activequant.core.domainmodel.events.OrderRejectEvent;
import org.activequant.core.domainmodel.events.OrderUpdateEvent;
import org.activequant.core.types.TimeStamp;
import org.activequant.util.log.IAQLogger;
import org.activequant.util.pattern.events.Event;
import org.activequant.util.pattern.events.IEventSource;
import org.apache.log4j.Logger;

/**
 * Base for Broker implementations. Takes care of tracker management.<br/>
 * An abstract class, implements the IBroker interface. Defines the OrderTrackerBase class. Holds the following associated variables:
 * <ul>
 * <li>logger(IAQLogger)</li>
 * <li>originalOrderMap(Map&lt;String,OrderTrackerBase&gt;)</li>
 * </ul>
 * <br>
 * <b>History:</b><br> 
 * - [11.12.2007] Created (Mike Kroutikov) <br>
 * - [07.05.2009] Added an assertion in prepareOrder (Ulrich Staudinger) <br> 
 * 
 * @author Mike Kroutikov
 */
public abstract class BrokerBase implements IBroker {
	/**
	 * protected final Logger log = Logger.getLogger(getClass());
	 */
	protected final Logger log = Logger.getLogger(getClass());
	/**
	 * private IAQLogger logger;
	 */
	private IAQLogger logger; 

	/**
	 * private static final double EPSILON = 1.0e-6;<br/>
	 * precision for double comparisons
	 */
	private static final double EPSILON = 1.0e-6;
	
	/**
	 * Implementation must return OrderTrackerBase possibly extended with 
	 * implementation-specific information.<br/>
	 * abstract method, needs to be implemented
	 * @param order order passed to <code>prepareTracker</code>.
	 * 
	 * @return implementation of tracker.
	 * @throws Exception if something goes wrong (i.e. order validation fails).
	 */
	protected abstract OrderTrackerBase createOrderTracker(Order order);
	/**
	 * public final Map&lt;String,OrderTrackerBase&gt; originalOrderMap = new ConcurrentHashMap&lt;String,OrderTrackerBase&gt;();
	 */
	public final Map<String,OrderTrackerBase> originalOrderMap = new ConcurrentHashMap<String,OrderTrackerBase>();
	/**
	 * returns the values from the associated orderMap(Map&lt;String,OrderTrackerBase&gt;) as an OrderTracker[] array
	 */
	public final IOrderTracker[] getOrders() {
		return originalOrderMap.values().toArray(new IOrderTracker[]{});
	}
	
	/**
	 * prepares an order for submittal: creates an order tracker by cloning this order. <br/>
	 * returns an OrderTrackerBase for the given order(Order) by invoking the abstract <code>OrderTrackerBase createOrderTracker(Order order)</code> method
	 */
	public IOrderTracker prepareOrder(final Order order) {
		// assertions 
		assert(order.getInstrumentSpecification()!=null) : "Instrument specification not present in order. ";
		// clone order here (caller might want to re-use order object).
		return createOrderTracker(new Order(order));
	}

	/**
	 * Extend this class to store implementation-specific 
	 * per-order private information, and implement order lifetime 
	 * transitions.<br/>
	 * An abstract class, implements the IOrderTracker interface. Holds the following associated variables:
	 * <ul>
	 * <li>orderEvent(Event&lt;OrderEvent&gt;)</li>
	 * <li>order(Order)</li>
	 * <li>isSubmitted(AtomicBoolean)</li>
	 * <li>isCanceled(AtomicBoolean)</li>
	 * <li>brokerAssignedId(String)</li>
	 * <li>completion(AtomicReference&lt;OrderCompletionEvent&gt;)</li>
	 * <li>commission(AtomicReference&lt;OrderCommissionEvent&gt;)</li>
	 * <li>openQuantity(double)</li>
	 * <li>totalQuantity(double)</li>
	 * <li>executions(List&lt;OrderExecutionEvent&gt;)</li>
	 * <li>commissions(List&lt;OrderCommissionEvent&gt;)</li>
	 * </ul>
	 */
	protected abstract class OrderTrackerBase implements IOrderTracker {
		/**
		 * protected final Logger log = Logger.getLogger(getClass());
		 */
		protected final Logger log = Logger.getLogger(getClass());
		/**
		 * private final Event&lt;OrderEvent&gt; orderEvent = new Event&lt;OrderEvent&gt;();
		 */
		private final Event<OrderEvent> orderEvent = new Event<OrderEvent>();
		/**
		 * private Order order;
		 */
		private Order order;
		/**
		 * private final AtomicBoolean isSubmitted = new AtomicBoolean(false);
		 */
		private final AtomicBoolean isSubmitted = new AtomicBoolean(false);
		/**
		 * private final AtomicBoolean isCanceled  = new AtomicBoolean(false);
		 */
		private final AtomicBoolean isCanceled  = new AtomicBoolean(false);
		/**
		 * protected String brokerAssignedId;
		 */
		protected String brokerAssignedId;
		/**
		 * private final AtomicReference&lt;OrderCompletionEvent&gt; completion = new AtomicReference&lt;OrderCompletionEvent&gt;();
		 */
		private final AtomicReference<OrderCompletionEvent> completion = new AtomicReference<OrderCompletionEvent>();
		/**
		 * private final AtomicReference&lt;OrderCommissionEvent&gt; commission = new AtomicReference&lt;OrderCommissionEvent&gt;();
		 */
		private final AtomicReference<OrderCommissionEvent> commission = new AtomicReference<OrderCommissionEvent>();
		/**
		 * private double openQuantity;<br/>
		 * to track when the order is filled/completed
		 */
		private double openQuantity;
		/**
		 * private double totalQuantity;<br/>
		 * to track when the order is filled as per IB (this is the total Order quantity to be filled, and it doesn't change unless I update the Order)
		 */
		private double totalQuantity;
		/**
		 * public final List&lt;OrderExecutionEvent&gt; executions = new ArrayList&lt;OrderExecutionEvent&gt;();<br/>
		 * I only need this for IBPaper Broker
		 */
		public final List<OrderExecutionEvent> executions = new ArrayList<OrderExecutionEvent>();
		/**
		 * private final List&lt;OrderCommissionEvent&gt; commissions = new ArrayList&lt;OrderCommissionEvent&gt;();
		 */
//		private final List<OrderCommissionEvent> commissions = new ArrayList<OrderCommissionEvent>();		
		/**
		 * Creates new tracker.<br/>
		 * constructs an OrderTrackerBase(abstract class, implements IOrderTracker) using the given order(Order) and its quantity(double) to set its associated order(Order) and openQuantity(double)
		 * @param order order.
		 */
		public OrderTrackerBase(Order order) {
			if(order.getOrderType() == null) {
				throw new NullPointerException("orderType can not be null");
			}

			if(order.getOrderSide() == null) {
				throw new NullPointerException("orderSide can not be null");
			}
			
			if(order.getInstrumentSpecification() == null)
			{
				throw new NullPointerException("instrumentSpec can not be null");
			}
		
			this.order = order;
			openQuantity = order.getQuantity();
			totalQuantity = openQuantity;
			if(openQuantity < EPSILON) {
				throw new IllegalArgumentException("illegal order quantity value: " + openQuantity);
			}
		}

		/**
		 * Implement response to user calling <code>submit()</code>.<br/>
		 * abstract method, needs to be implemented
		 * @return broker assigned id (must be unique).
		 * @throws Exception if something does not go right.
		 */
		protected abstract String handleSubmit();
		
		/**
		 * Implement response to user calling <code>update()</code>.<br/>
		 * abstract method, needs to be implemented
		 * @param newOrder new order.
		 * @throws Exception if something goes wrong.
		 */
		protected abstract void handleUpdate(Order newOrder);

		/**
		 * Implement response to user calling <code>cancel()</code>.<br/>
		 * abstract method, needs to be implemented
		 * @throws Exception if something goes wrong.
		 */
		protected abstract void handleCancel();
		/**
		 * <strong>1.</strong> If the associated isCanceled(AtomicBoolean) is true, it throws an IllegalStateException("order was canceled: no more action expected")<br/>
		 * <strong>2.</strong> The associated isSubmitted(AtomicBoolean) must be false and then is changed to true, otherwise it throws an IllegalStateException("order was already submitted")<br/>
		 * <strong>3.</strong> Invokes the abstract <code>handleSubmit()</code> method and sets the associated brokerAssignedId(String) with the result<br/>
		 * <strong>4.</strong> Maps this OrderTrackerBase into the associated originalOrderMap(Map&lt;String,OrderTrackerBase&gt;) using the associated brokerAssignedId(String) as key. (this key mustn't already exist)
		 */
		public final void submit() {
			if(isCanceled.get()) {
				throw new IllegalStateException("order was canceled: no more action expected");
			}
			if(isSubmitted.getAndSet(true)) {
				throw new IllegalStateException("order was already submitted");
			}
			//this has the form "IB-${orderId}"
			brokerAssignedId = handleSubmit();
			if(brokerAssignedId == null) {
				throw new NullPointerException("broker assigned id can not be null!");
			}

			if(originalOrderMap.containsKey(brokerAssignedId)) {
				throw new IllegalStateException("duplicate broker assigned id: " + brokerAssignedId);
			}
			
			originalOrderMap.put(brokerAssignedId, this);
		}
		/**
		 * <strong>1.</strong> If the associated isSubmitted(AtomicBoolean) is false, it sets the associated order(Order) with a copy of the given newOrder(Order) and returns without doing anything else.<br/>
		 * <strong>2.</strong> Otherwise, it validates that the orderType(OrderType) and orderSide(OrderSide) of the given newOrder(Order) match the ones in the associated order(Order).
		 * The associated isCanceled(AtomicBoolean) must be false as well. The associated completion(AtomicReference&lt;OrderCompletionEvent&gt;) can't have a value too, otherwise it
		 * returns without doing anything.<br/>
		 * If all these checks pass, it invokes the abstract <code>handleUpdate(Order)</code> method using a copy of the given newOrder(Order)
		 */
		public final void update(Order newOrder) {
			if(!isSubmitted.get()) {
				// prior to order submission update is transparent and purely local
				order = new Order(newOrder);
				return;
			}

			// validate update
			if(order.getOrderType() != newOrder.getOrderType()) {
				throw new IllegalArgumentException("Can not update order type");
			}
			if(order.getOrderSide() != newOrder.getOrderSide()) {
				throw new IllegalArgumentException("Can not update order side");
			}

			if(isCanceled.get()) {
				throw new IllegalStateException("order was canceled: no more action expected");
			}

			if(completion.get() != null) {
				// completed
				return;
			}

			handleUpdate(new Order(newOrder));
		}
		/**
		 * <strong>1.</strong> If the associated isSubmitted(AtomicBoolean) is false, throws an IllegalStateException("order was not submitted"). The associated completion(AtomicReference&lt;OrderCompletionEvent&gt;) can't have a value too, otherwise it
		 * returns without doing anything.<br/>
		 * <strong>2.</strong> The associated isCanceled(AtomicBoolean) must be false and then is changed to true, otherwise it returns without doing anything.<br/>
		 * If all these checks pass, it invokes the abstract <code>handleCancel()</code> method
		 */
		public final void cancel() {
			if(!isSubmitted.get()) {
				throw new IllegalStateException("order was not submitted");
			}
			
			if(completion.get() != null) {
				// completed
				return;
			}

			if(isCanceled.getAndSet(true)) {
				return;
			}
			
			handleCancel();
		}
		/**
		 * Returns an OrderCompletionEvent whose averagePrice(double), totalQuantity(double), totalCommission(double) and eventTimeStamp(TimeStamp) are calculated from the
		 * associated executions(List&lt;OrderExecutionEvent&gt;).<br/>
		 * The given errorEvent(OrderEvent) is optional and is used to set the terminalError(OrderEvent) of the OrderCompletionEvent
		 * @param errorEvent
		 * @param lastExecution
		 * @return
		 */
		private OrderCompletionEvent prepareCompletion(OrderEvent errorEvent, OrderExecutionEvent lastExecution) {
			OrderCompletionEvent completion = new OrderCompletionEvent();
			completion.setTerminalError(errorEvent);
			
			TimeStamp completionTimeStamp = null;
			
			// compute total price and total quantity of the executions
			double totalQuantity = 0.0;
			double totalCost = 0.0;
			//double totalCommission = 0.0;
			
//			for(OrderExecutionEvent e : executions) {
//				OrderExecutionEvent execution = (OrderExecutionEvent) e;
//				totalQuantity += execution.getQuantity();
//				totalCost += execution.getQuantity() * execution.getPrice();
//				//totalCommission += execution.getCommission();
//				completionTimeStamp = execution.getEventTimeStamp();
//			}
			if(lastExecution!=null){
				totalQuantity=lastExecution.getCumulatedQuantity();
				totalCost += lastExecution.getCumulatedQuantity() * lastExecution.getAveragePrice();
				completionTimeStamp = lastExecution.getEventTimeStamp();
			}
			
			if(totalQuantity > EPSILON) {
				completion.setAveragePrice(totalCost / totalQuantity);
			} else {
				completion.setAveragePrice(0.0);
			}
			
			if(errorEvent != null) {
				completionTimeStamp = errorEvent.getEventTimeStamp();
			}
			
			completion.setTotalQuantity(totalQuantity);
			//completion.setTotalCommission(totalCommission);
			completion.setEventTimeStamp(completionTimeStamp);

			return completion;
		}

		// FIXME: incorrect OO - should use polymorfphism instead
		/**
		 * <strong>1.</strong> If the given event(OrderEvent) is an OrderRejectEvent or OrderCancelEvent, it returns an OrderCompletionEvent whose averagePrice(double), totalQuantity(double), totalCommission(double) and eventTimeStamp(TimeStamp) are calculated from the
		 * associated executions(List&lt;OrderExecutionEvent&gt;). The given event(OrderEvent) is used to set the terminalError(OrderEvent) of the OrderCompletionEvent<br/>
		 * <strong>2.</strong> If the given event(OrderEvent) is an OrderUpdateEvent, it updates/sets the associated openQuantity(double) with the quantity(double) of the updated(Order) and returns null<br/>
		 * <strong>3.</strong> If the given event(OrderEvent) is an OrderExecutionEvent, it adds it to the associated executions(List&lt;OrderExecutionEvent&gt;) and decreases/subtracts its quantity(double)
		 * from the associated openQuantity(double) and if the remaining openQuantity(double) is less than EPSILON(1.0e-6) it returns an OrderCompletionEvent whose averagePrice(double), totalQuantity(double), totalCommission(double) and eventTimeStamp(TimeStamp) are calculated from the
		 * associated executions(List&lt;OrderExecutionEvent&gt;).
		 */
		private OrderCompletionEvent checkForCompletion(OrderEvent event) {
			
			if(event instanceof OrderRejectEvent) {
				return prepareCompletion(event, null);
			} else if(event instanceof OrderCancelEvent) {
				return prepareCompletion(event, null);
			} else if(event instanceof OrderUpdateEvent) {
				Order newOrder = ((OrderUpdateEvent) event).getUpdatedOrder();
				openQuantity = newOrder.getQuantity();
				totalQuantity = openQuantity;
				return null;
			} else if(event instanceof OrderExecutionEvent) {
				OrderExecutionEvent execution=(OrderExecutionEvent) event;
				executions.add(execution);// i only need this for IBPaper Broker
				openQuantity -= execution.getQuantity();
				if(openQuantity < EPSILON) {
					// completed!
					return prepareCompletion(null, execution);
				}
				//check from IB's side?
				double cumQty=execution.getCumulatedQuantity();//maybe I should use this only?
				if(Math.abs(totalQuantity-cumQty)< EPSILON){
					// completed!
					return prepareCompletion(null, execution);
				}
			}
			return null;
		}
		
		private OrderCommissionEvent checkForTotalCommission(OrderCommissionEvent commissionEvent) {			
//			commissions.add(commissionEvent);
			if(commission.get()==null && completion.get()!=null && openQuantity < EPSILON /*&& commissions.size()>0 && executions.size() == commissions.size()*/) {
				// got a commission report for every execution!				
				commissionEvent.setFinalCommissionEvent(true);//commissions are for each execution so far not for each executions separately
				return commissionEvent;
			}
			return null;
		}

		/**
		 * Use this method to fire one of the order events. Note that
		 * base will take care of adding it to the OrderTicket, and
		 * generating completion, if necessary.<br/>
		 * <strong>1.</strong> Logs the given event(OrderEvent) into the associated logger(IAQLogger) if possible. The associated completion(AtomicReference&lt;OrderCompletionEvent&gt;) can't
		 * have a value otherwise it returns without doing anything.<br/>
		 * <strong>2.</strong> Invokes the <code>orderEvent.fire(event)</code> method on the associated orderEvent(Event&lt;OrderEvent&gt;) using the given event(OrderEvent)<br/>
		 * <strong>3.</strong> If the given event(OrderEvent) completes the order it gets the resulting OrderCompletionEvent and sets it as the current value in the associated 
		 * completion(AtomicReference&lt;OrderCompletionEvent&gt;) and invokes the <code>orderEvent.fire(orderCompletionEvent)</code> method on the associated orderEvent(Event&lt;OrderEvent&gt;)
		 * 
		 * @param event order-related event.
		 */
		protected final synchronized void fireOrderEvent(OrderEvent event) {
			if(log.isDebugEnabled())
				log.debug("dispatching new order event: " + event);
			if(logger!=null)
				logger.log(order, event);
			if(completion.get() != null && !(event instanceof OrderCommissionEvent) && !(event instanceof OrderCancelEvent)) { //only allow OrderCommissionEvents or OrderCancelEvents after an OrderCompletitionEvent has been fired/set 
				log.warn("new event arrived on a completed ticket (dropped): " + event);
				return;
			}
			
			try {
				orderEvent.fire(event);
			} catch(Exception ex) {
				ex.printStackTrace();
				log.error(ex);
			}

			// was it a terminal event?
			if(this.completion.get() == null){
				OrderCompletionEvent completionEvent = checkForCompletion(event);
				if(completionEvent != null) {
					this.completion.set(completionEvent);
					if(log.isDebugEnabled())
						log.debug("order completed (about to dispatch completion): " + completionEvent);
					try {
						orderEvent.fire(completionEvent);
					} catch(Exception ex) {
						ex.printStackTrace();
						log.error(ex);
					}
				}
			}
			
			if(event instanceof OrderCommissionEvent){
				OrderCommissionEvent commissionEvent = (OrderCommissionEvent) event;
				OrderCommissionEvent totalCommissionEvent=checkForTotalCommission(commissionEvent);
				if(totalCommissionEvent != null) {
					this.commission.set(totalCommissionEvent);
					if(log.isDebugEnabled())
						log.debug("order commission completed (about to dispatch commission): " + totalCommissionEvent);
					try {
						orderEvent.fire(totalCommissionEvent);
					} catch(Exception ex) {
						ex.printStackTrace();
						log.error(ex);
					}
				}
			}
		}
		/**
		 * returns the associated brokerAssignedId(String)
		 */
		public final String getBrokerAssignedId() {
			return brokerAssignedId;
		}
		/**
		 * returns the associated order(Order)
		 */
		public final Order getOrder() {
			return order;
		}
		/**
		 * returns the associated orderEvent(Event&lt;OrderEvent&gt;)
		 */
		public IEventSource<OrderEvent> getOrderEventSource() {
			return orderEvent;
		}
		/**
		 * returns the current OrderCompletionEvent value from the associated completion(AtomicReference&lt;OrderCompletionEvent&gt;)
		 */
		public final OrderCompletionEvent getOrderCompletion() {
			return completion.get();
		}
	}
	

	/**
	 * the AQ Logger implementation. <br/>
	 * returns the associated logger(IAQLogger)
	 * @return
	 */
	public IAQLogger getLogger() {
		return logger;
	}

	/**
	 * injector for the AQ logger.<br/>
	 * sets the associated logger(IAQLogger) with the given logger(IAQLogger)
	 * @param logger
	 */
	public void setLogger(IAQLogger logger) {
		this.logger = logger;
	}
	
}
